﻿#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>



//<1>指针变量和地址
//int main()
//{
//	int a = 0;//变量创建的本质是在内存中开辟一个空间
//	printf("%p\n", &a);//%p打印前面不省略0，且大写地址
//	//内存每次给变量分配的地址（指针）都是不一样的
//	printf("%x", &a);//%x打印前面省略0，且小写地址
//
//	return 0;
//}
			//注：（1）内存会被划分为一个个单元，每个内存单元的大小都是一个字节
			//	（2）每个内存单元都会给一个编号--也叫地址--也是指针
			//	（3）如果一个变量占4个字节，也就是4个内存单元，有4个地址，以取最小的地址为准



//<2>指针变量的创建
//int main()
//{
//	int a = 0;
//
//	int* p = &a;//存放一个变量的地址要&（取地址）
//	//创建一个变量来存放a的地址，这个变量就是指针变量
//	//int表示指针变量指向的对象是（即a）是int类型
//	//*表示p是指针变量
//	//注：p（指针变量）的类型是int * 
//	return 0;
//}



//<3>解引用操作符
//int main()
//{
//	int a = 0;
//	int* p = &a;
//	*p = 100;//*指针变量（*p）是解引用操作
//			//*-->解引用操作符
//	//解引用操作：通过解引用操作对指针变量存放的地址进行间接访问，相当于*p = a
//	printf("%d", a);
//
//	return 0;
//}



//<4>指针变量的大小
//int main()
//{
//	//int a = 0;
//	//int* p = &a;
//	//
//	//printf("%zd\n", sizeof(p));
//	//printf("%zd\n", sizeof(int*));
//	//指针变量的大小取决于他存放了什么，而指针变量存放的是指针（地址），所以指针变量的大小取决于地址的大小
//	//在32位机器上存放的地址大小是32个bit位，也就是4个字节，所以32位机器上，指针变量的大小是4个字节
//	//在64位机器上存放的地址大小是64和bit位，也就是8个字节，所以64位机器上，指针变量的大小是8个字节
//  //验证：打印地址后发现VS编译器的地址查看是十六进制，32个bit位机器存放的是8个十六进制地址位，而一个十六进制地址位，是4个二进制地址位，相当于就是32个二进制位
//  //		放在64位机器上也是相同的原理，打印地址有十六个十六进制位，就是64个二进制位
// 
// 
//	//注：指针变量的大小以及指针变量的类型大小取决于地址，不会被所指向的变量的类型所影响
//
//	//比如说：
//	char b = 'w';
//	char* pb = &b;
//	printf("%zd\n", sizeof(pb));//8
//	printf("%zd", sizeof(char*));//8
//
//	return 0;
//}



//<5>指针变量的类型的意义
//int main()
//{
//	int a = 0x11223344;//把4个字节的a填满，在x86环境下好观察
//	int* pa = &a;
//	*pa = 0;
//
//	return 0;
//}

//int main()
//{
//	int a = 0x11223344;//把4个字节的a填满，在x86环境下好观察
//	char* pa = &a;
//	*pa = 0;
//
//	return 0;
//}
	//结论：通过两段代码的比较，可以发现指针类型决定了指针进行解引用操作时，会访问多大的空间（由指针类型的指向对象的类型决定）
			//比如：int a = 10;如果指针类型的指向对象的类型是char，那么访问的大小是本身类型的大小（char是1个字节的大小，就只会访问1个字节的大小）
	//注意：指针类型的指向对象的类型的大小要和指针类型的大小区分开，指向对象的类型的大小是用于决定访问的大小，而指针类型的大小是用于决定存放地址的大小



//<6>指针+-整数
//int main()
//{
//	int a = 10;
//	int* p1 = &a;
//	char* p2 = &a;
//	printf("p1 = %p\n", p1);
//	printf("p1+1 = %p\n", p1 + 1);
//	//可以发现：p1+1跳过了4个字节的地址
//
//	printf("p2 = %p\n", p2);
//	printf("p2+1 = %p\n", p2 + 1);
//	//而p2+1只跳过了1个字节的地址
//
//	//因为p1，p2的指向对象的类型不同，一个是int，一个是char
//	//所以：指向对象的类型本身的大小还决定了指针向前或向后的步长
//
//	return 0;
//}



//<7>void *
//int main()
//{
//	int a = 0;
//	int* p1 = &a;
//	//char* p2 = &a;//指针类型所指向的对象的类型是int，如果用char会报警告：从int*(所指向的对象本来是int类型，要用int*)到char*的类型不兼容
//	void* pv = &a;//如果用void*就不会报警告，因为void*是无具体类型的指针类型，泛型指针类型，可以用来接收任意类型的地址
//	//但局限的是：void*接收地址后，并不能知道接收了什么类型的地址，因此无法确定在指针的步长和解引用操作中的大小，所以不能进行指针的+-步长，解引用操作
//
//
//	return 0;
//}



//<8>const修饰变量
//int main()
//{
//	//int a = 0;
//	//a = 10;//a初始化为0，然后再赋值改变a的大小，是没有问题的
//
//	const int a = 0;//但是当我用const修饰变量a，变量a就不能直接赋值改变了
//					//但变量a仍然是变量，而不是常量，是常变量，具有常属性的变量
//	a = 10;//err
//	printf("%d\n", a);
//
//	return 0;
//}

//证明const修饰的变量a仍然是变量：
//int main()
//{
//	//int a = 10;
//	const int a = 10;
//	int arr[a] = { 0 };//数组中元素的个数是不能用变量来表示的
//						//加上const修饰的a，仍然会报警告，说明a仍然是变量，而不是常量
//
//	return 0;
//}

//当const修饰的a不能直接赋值改变，那应该怎么改变呢？
//int main()
//{
//	const int a = 0;
//	int * p1 = &a;//存储a的地址
//	*p1 = 10;//解引用该地址的变量a
//			//就可以访问变量a，进行间接赋值
//	printf("%d\n", a);
//
//	return 0;
//}

//那能不能阻止解引用进行的间接赋值改变呢，看<9>



//<9>const修饰指针变量
//const修饰指针变量有三种情况
//（1）const在*左边
//int const * p = &a;
//ocnst int * p = &a;
// 
//（2）const在*右边
// int * const p =&a;
// 
// (3)const在*左右边
// int const * const p = &a;
// 
//int main()
//{
//	int a = 0;
//	int const *p1 = &a;//当const在*左边时，修饰的是*p1，对*p1进行限制，而*p1表示所指向的对象，因此所指向的对象被限制了，指向对象的内容不能进行改变
//	//*p1 = 10;//所指向的对象被限制了，不能进行改变
//	int m = 10;
//	p1 = &m;//但是p1没有被限制，p1表示指针变量，指针变量本身的内容(存放的地址)可以改变
//	printf("%d\n", a);
//	return 0;
//}

//int main()
//{
//	int a = 0;
//	int* const p1 = &a;//当const在*右边时，修饰的是p1，对p1进行限制，而p1表示指针变量，因此指针变量被限制了，本身内容不能进行改变
//	
//	*p1 = 10;//但是*p1没有被限制，*p1表示所指向的对象，因此所指向的对象的内容可以进行改变
//	int m = 0;
//	//p1 = &m;//指针变量被限制了，本身内容不能进行改变
//	printf("%d\n", a);
//
//	return 0;
//}

//int main()
//{
//	int a = 0;
//	int const* const p1 = &a;//当const在*左右两边都有时，同时修饰了*p1和p1
//							//也就是同时限制了*p1和p1，意味着所指向的对象的内容不可以改变，指针变量本身的内容也不可以改变
//	*p1 = 10;//所指向的对象的内容不可以改变
//	int m = 0;
//	p1 = &m;//指针变量本身的内容也不可以改变
//
//
//	return 0;
//}



//<10>指针运算：指针+-整数

//打印数组中的每个元素
//int main()
//{
//	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//	//打印数组中的每个元素
//	int i = 0;
//	int sz = sizeof(arr) / sizeof(arr[0]);
//	for (i = 0; i < sz; i++)
//	{
//		printf("%d ", arr[i]);
//	}
//
//	return 0;
//}

//利用指针来打印数组中的每个元素
//int main()
//{
//	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };//数组元素的下标是从低到高连续的，并且所对应的地址也是从低到高连续的
//	int* p1 = &arr[0];//arr也要取地址
//					//存储第一个元素的地址
//	int i = 0;//创建变量表示下标
//	int sz = sizeof(arr) / sizeof(arr[0]);//数组元素个数
//	for (i = 0; i < sz; i++)
//	{
//		printf("%d ", *p1);//解引用操作，打印所指向的对象
//		p1++;//每打印完一次，地址就会增大1步（4个字节，由数组中的元素类型决定的（int类型））
//	}
//
//	return 0;
//}

//int main()
//{
//	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };//数组元素的下标是从低到高连续的，并且所对应的地址也是从低到高连续的
//	int* p1 = &arr[0];//arr也要取地址
//	//存储第一个元素的地址
//	int i = 0;//创建变量表示下标
//	int sz = sizeof(arr) / sizeof(arr[0]);//数组元素个数
//	for (i = 0; i < sz; i++)
//	{
//		printf("%d ", *(p1 + i));//也可以这样简写，来进行地址上步长的增加，先进行步长的增加，再解引用操作，打印指向的对象
//									//解引用操作，打印所指向的对象
//		
//	}
//
//	return 0;
//}



//<11>指针-指针
//int main()
//{
//	int arr[10] = { 0 };
//	printf("%d\n", &arr[9] - &arr[0]);
//	printf("%d\n", &arr[0] - &arr[9]);
//	//指针-指针得到的是步长（这里的步长指的是元素个数）
//	//指针-指针的前提是面向同一块空间（注：同一种类型还不能满足），否则没有意义
//
//	//比如：用arr2的第一个元素地址减去arr的第一个元素地址，算出了16，这个16指的是地址之间的差值，但并不知道哪个元素的地址更低或更高，并且也不能算作步长来看待
//	//没有实际意义，这是因为两个数组不是同一块空间所导致的，所以指针-指针要在同一块空间进行
//	int arr2[5] = { 0 };
//	printf("%d\n", &arr2[0] - &arr[0]);
//
//	//再比如：用arr3的第一个元素减去arr的第一个元素，得到的是-17.......的杂数，因为没有面向同一块空间，，并且两个空间的类型都不同
//	char arr3[3] = { 0 };
//	printf("%d\n", &arr3[0] - arr[0]);
//	return 0;
//}

//利用指针-指针求字符串的字符个数
#include <string.h>

//int my_strlen(char* p1)//所以这边传参的时候，传递的参数是类型 + *（表示右边的变量是指针变量）+ 变量名
//{
//	int* p2 = p1;//这边不能&p1，因为要的是将一开始的字符串元素地址存储下来，到后面进行字符串最后元素的地址减去字符串一开始元素的地址的操作
//					//所以应该存储字符串的第一个元素的地址，而不是指针变量本身的地址
//	while (*p1 != '\0')//这边要进行解引用操作，不能把地址拿去与字符比较，而是拿指向对象（这边是字符）拿去与字符比较
//	{
//		p1++;
//	}
//	return p1 - p2;//字符串最后一个元素的地址减去第一个元素的地址，得出步长（字符的个数）
//}
//
//int main()
//{
//	char arr[] = "abcdef";
//	int num = my_strlen(arr);//单独一个数组名存放到函数中，表示的是数组第一个元素的地址：&arr[0]
//
//	printf("%d\n", num);
//
//	return 0;
//}



//<12>指针的关系运算
//int main()
//{
//	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//	//打印数组中的10个元素
//	int* p1 = &arr[0];
//	int sz = sizeof(arr) / sizeof(arr[0]);
//	while (p1 < arr + sz)//p1和arr + sz的就是两个指针（地址）的关系，比较大小的关系
//						//单独一个数组名arr表示数组中第一个元素的地址：即&arr[0]
//						//不用 = ，因为每个元素的前面的那根线才是属于那个元素的地址，后面的线不是
//	{
//		printf("%d ", *p1);
//		p1++;
//	}
//
//	return 0;
//}



//<13>野指针--野指针就是指针指向的位置是不可知的
//（1）指针未初始化-->野指针
//int main()
//{
//	int* p1;//这里p1是局部变量，局部变量不进行初始化，会生成随机值，但在VS2022上都不让运行
//				//这里指针未初始化
//	*p1 = 10;
//				//导致指向的位置是不可知的（这里是随机的），不能访问空间，也就不能改变相应空间的值
//	printf("%d ", *p1);
//
//	return 0;
//}

//（2）指针越界访问--野指针
//#include <stdio.h>
//int main()
//{
//	int arr[10] = { 0 };
//	int* p = &arr[0];
//	int i = 0;
//	for (i = 0; i <= 11; i++)
//	{
//		//当指针指向的范围超出数组arr的范围时，就会导致指针指向的位置是不可知的，p就是野指针
//		*(p++) = i;
//	}
//	return 0;
//}

//（3）指针指向的空间释放
//#include <stdio.h>
//int* test()
//{
//	int n = 100;
//	return &n;
//}
//int main()
//{
//	int* p = test();//进入test()，返回&n给p
//	printf("%d\n", *p);//这时候test()函数运行结束，销毁了，因此n变量也销毁了，存储的100也销毁了
//	//p仍然存放着&n，但是当p解引用去访问&n时，即使地址是原来一样的，但n不存在了，存放的100也不存在了，因此p指针指向的位置是不可知的，p就是野指针
//	return 0;
//}



//<14>如何有效规避野指针
//（1）指针初始化（针对指针未初始化）
//int main()
//{
//	int a = 0;//如果知道指针指向的位置，并且该位置是可知的
//	int* p1 = &a;//那么可以直接赋值地址给指针，进行初始化
//	*p1 = 20;
//
//	return 0;
//}

//int main()
//{
//	int* p2 = NULL;//如果不知道指针应该指向什么位置，那就直接赋值NULL给指针，进行初始化
//					//注：NULL的本质是0，0也是地址
//					//但是这个地址无法使用，使用会报错，所以可以直接赋值给NULL，但是不能对其进行地址的访问
//	//*p2 = 20;//err
//	return 0;
//}	

//注：两种初始化都赋值地址给了指针，只是第一种地址是有用的，可以访问该地址的空间，第二种地址是无用的，不能够访问该地址的空间

//（2）小心指针越界（针对指针越界访问）
//1.一个程序向内存申请了哪些空间，通过指针也就只能访问那些空间（把指针访问什么空间的代码写好）

//2.置NULL，指针使⽤之前检查有效性
//int main()
//{
//	int arr[10] = { 1,2,3,4,5,67,7,8,9,10 };
//	int* p = &arr[0];
//	int i = 0;
//	for (i = 0; i < 10; i++)
//	{
//		*(p++) = i;
//	}
//	//此时p已经越界了，可以把p置为NULL
//	p = NULL;
//	//下次使⽤的时候，判断p不为NULL的时候再使⽤
//	//...
//	p = &arr[0];//重新让p获得地址
//	if (p != NULL) //判断
//	{				//无法去规定一个野指针，再加入if语句中进行判断，但是可以规定一个NULL（空指针），空指针是无效的，在指针出现问题时，进行赋值NULL，把指针认定为无效
//					//从而不会影响到下面的代码，当重新要使用这个指针时，又可以再次让指针获得地址，再进行if语句的判断，重新开始使用指针
//					//注：我们无法规定一个野指针，在程序中进行错误指针的阻拦；我们只能规定一个空指针（NULL），发现了野指针，再进行赋值NULL，进行阻拦（if，assert）
//		//...
//	}
//	return 0;
//}

//（3）避免返回局部变量的地址（针对指针指向的空间释放）
//#include <stdio.h>
//int* test()
//{
//	int n = 100;
//	return &n;//可以返回局部变量，但是不能返回局部变量的地址
//}
//int main()
//{
//	int* p = test();//进入test()，返回&n给p
//	printf("%d\n", *p);//这时候test()函数运行结束，销毁了，因此n变量也销毁了，存储的100也销毁了
//	//p仍然存放着&n，但是当p解引用去访问&n时，即使地址是原来一样的，但n不存在了，存放的100也不存在了，因此p指针指向的位置是不可知的，p就是野指针
//	return 0;
//}



//<15>assert断言

//int main()
//{
//	int a = 0;
//	int* p1 = &a;
//	assert(p1 != NULL);//assert是断言，只有断言为真，才能继续执行代码，否则直接终止，进行报错
//	*p1 = 10;			//assert要包含头文件<assert.h>
//	printf("%d ", *p1);
//
//	return 0;
//}

//int main()
//{
//	int* p1 = NULL;//现在p1为空指针
//	assert(p1 != NULL);//断言为假，进行报错
//						//assert是断言，只有断言为真，才能继续执行代码，否则直接终止，进行报错
//	*p1 = 10;			//assert要包含头文件<assert.h>
//	printf("%d ", *p1);
//
//	return 0;
//}

//int main()
//{
//	int* p1;//这边为野指针，assert就不会进行报错，因为我们无法规定野指针并在程序中实现阻拦，我们只能自己辨别野指针，然后设立空指针，进行阻拦
//	assert(p1 != NULL);//assert是断言，只有断言为真，才能继续执行代码，否则直接终止，进行报错
//	*p1 = 10;			//assert要包含头文件<assert.h>
//	printf("%d ", *p1);
//
//	return 0;
//}

//assert不仅可以用于野指针的拦截，还可以用于其他方面，只要是不符合assert中的条件，就会进行阻拦
//int main()
//{
//	int a = 0;
//	scanf("%d", &a);//若输入0，assert断言为假，就会出错
//	assert(a != 0);
//	printf("%d ", a);
//
//	return 0;
//}

//如果在assert的头文件#include <assert>前面加上#define NDEBUG，就会禁用文件中所有的assert语句
#define NDEBUG
#include <assert.h>
//int main()
//{
//	int a = 0;
//	scanf("%d", &a);//若输入0，assert断言为假，就会出错
//	assert(a != 0);//但是我加了#define NDEBUG，assert语句失效，即使输入了0，也不会报错
//	printf("%d ", a);
//
//	return 0;
//}



//<16>strlen的模拟实现优化
//size_t my_strlen(const char* s)//加上const表示*s不能被修改
//{
//	size_t count = 0;//size_t是sizeof的返回类型，sizeof的返回类型可能是unsigned int ，也有可能是 unsigned long ，甚⾄是 unsigned long long
//					//有三个返回类型，不利于程序的可移植性，于是规定了size_t来统一表示这三个类型
//					//这边用size_t是用来表示无符号整型，以便能够存储更大的值
//	assert(s != NULL);//assert进行断言，防止主函数my_strlen传入NULL
//	while (*s != '\0')
//	{
//		count++;
//		s++;
//	}
//	return count;
//}
//
//int main()
//{
//	//strlen() - 求字符串的长度 -  统计的是字符串中\0之前的字符的个数
//	char arr[] = "abcdef";
//	size_t len = my_strlen(arr);
//	printf("%zd\n", len);
//
//	return 0;
//}



//<17>写一个函数，交换两个整型变量的值
//void Swap(int x, int y)
//{
//	int z = 0;
//	z = x;
//	x = y;
//	y = z;
//}
//
//int main()
//{
//	int a = 10;
//	int b = 20;
//	printf("交换前：a = %d  b = %d\n", a, b);
//	void Swap(a, b);//传值调用，将变量本身传递给了函数
//	printf("交换后：a = %d  b = %d\n", a, b);
//
//	return 0;
//}
//调试后可以发现：x，y的值交换了，但是a，b的值没有交换
//因为对应的实参和形参拥有各自独立的空间，各自的地址是不同的
//当实参传给形参时，形参只是实参的一份临时拷贝
//形参的改变不会影响实参的改变
//所以这里不能交换a和b的值

//void Swap2(int* p1, int* p2)//指针变量接收地址
//{
//	int z = 0;
//	z = *p1;//z = a;解引用操作
//	*p1 = *p2;//a = b;
//	*p2 = z;//b = z;
//}
//
//int main()
//{
//	int a = 10;
//	int b = 20;
//	printf("交换前：a = %d  b = %d\n", a, b);
//	Swap2(&a, &b);//传地址：传址调用，将变量的地址传递给了函数
//	printf("交换后：a = %d  b = %d\n", a, b);
//
//	return 0;
//}
//既然是因为实参和形参的地址不同，空间不同，才导致的实参无法交换
//那就把实参的地址传到函数中，再用形参创建的指针变量来接收地址
//再运用解引用操作，把指向的对象进行交换
//因为地址（空间）是相同的，所以形参的交换可以影响到实参，因此有用

//能否将a，b的地址进行交换，从而实现a，b值的交换
//不能
//因为每个值下面都有地址
//当形参要与实参发生真正的联系，形参和实参的地址要是一样的
//当把实参的地址传到形参中，才发生了真正的联系
//但如果我把形参（*p1，*p2）接收到的地址（p1，p2）拿去交换，那么该地址的地址与实参是不一样的，因为*p1，*p2的地址才与a，b相同，而不是p1，p2
//而且地址本身就是一个值，地址（p1，p2）的下面还有一个地址（指针变量本身也是有地址的），这个地址与实参地址是不同的

//void Swap2(int* p1, int* p2)//指针变量接收地址
//{							//*p1，*p2是形参，而不是p1，p2
//	int p = NULL;
//	p = p1;
//	p1 = p2;
//	p2 = p;
//}
//
//int main()
//{
//	int a = 10;
//	int b = 20;
//	printf("交换前：a = %d  b = %d\n", a, b);
//	Swap2(&a, &b);//传地址：传址调用，将变量的地址传递给了函数
//	printf("交换后：a = %d  b = %d\n", a, b);
//
//	return 0;
//}
//遇到这种问题首先明白：在主调函数和函数之间，什么是实参，什么是形参
//然后明确问题：把形参的地址进行交换，能否带来实参数值上的交换
//然后分析：进行交换的，操作的参数是不是形参，它的地址跟实参相同吗
// 形参的地址确实与实参的地址是一样的，但是形参地址的地址，也就是形参指针变量的地址与实参的地址是不同的，进行形参地址的交换，无法影响到实参
//本质上要求的是地址的是否相同，不相同，就无法改变，相同地址，才能改变